/*
 * File     : AbstractPlatform.java
 * Created  : 16 Apr 2011
 *
 * Copyright © 2011 Matthew Wilson (mj. {my-surname} .uk {at} gmail.com)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.googlecode.dni.internal.platform;

import java.util.Collections;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import com.googlecode.dni.internal.DniInternal;
import com.googlecode.dni.internal.library.DynamicLibrary;
import com.googlecode.dni.library.CallingConvention;
import com.googlecode.dni.library.SystemException;
import com.googlecode.dni.platform.PlatformGroup;
import com.googlecode.dni.platform.PlatformType;
import com.googlecode.dni.type.Pointer;
import com.googlecode.dni.type.structure.Padding;


/**
 * <p>
 *  Base implementation of {@link Platform}.
 * </p>
 *
 * @author Matthew Wilson
 */
abstract class AbstractPlatform implements Platform
{

    private final PlatformGroup platformGroup;

    private final PlatformType platformType;

    private final DynamicLibrary dniDynamicLibrary;

    private final Padding defaultPadding;

    private Set< CallingConvention > supportedCallingConventions;

    private Map< Class< ? >, Integer > nativeSizes;

    private String defaultCharset;

    private final String wideCharset;

    private int pageSize;


    /**
     * @param platformGroup
     *            the platform group
     * @param platformType
     *            the platform type
     * @param dniDynamicLibrary
     *            the DNI dynamic library
     * @param defaultPadding
     *            the default padding
     * @param wideCharset
     *            the wide character set
     */
    AbstractPlatform( final PlatformGroup platformGroup,
                      final PlatformType platformType,
                      final DynamicLibrary dniDynamicLibrary,
                      final Padding defaultPadding,
                      final String wideCharset )
    {
        this.platformGroup = platformGroup;
        this.platformType = platformType;
        this.dniDynamicLibrary = dniDynamicLibrary;
        this.defaultPadding = defaultPadding;
        this.wideCharset = wideCharset;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public final PlatformGroup getPlatformGroup()
    {
        return this.platformGroup;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public final PlatformType getPlatformType()
    {
        return this.platformType;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public final DynamicLibrary getDniDynamicLibrary()
    {
        return this.dniDynamicLibrary;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public final Map< Class< ? >, Integer > getNativeSizes()
    {
        Map< Class< ? >, Integer > nativeSizesLocal = this.nativeSizes;
        if ( nativeSizesLocal == null )
        {
            nativeSizesLocal = PlatformUtilities.getNativeSizes();
            this.nativeSizes = nativeSizesLocal;
        }
        return this.nativeSizes;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public final Padding getDefaultPadding()
    {
        return this.defaultPadding;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public final String getDefaultCharset()
    {
        String charset = this.defaultCharset;
        if ( charset == null )
        {
            charset = getDefaultCharsetImpl();
            if ( charset == null )
            {
                throw new AssertionError( "Failed to determine default charset for platform" );
            }

            this.defaultCharset = charset;
        }
        return charset;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public final String getWideCharset()
    {
        return this.wideCharset;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public final Set< CallingConvention > getSupportedCallingConventions()
    {
        Set< CallingConvention > set = this.supportedCallingConventions;
        if ( set == null )
        {
            set = EnumSet.allOf( CallingConvention.class );
            for ( Iterator< CallingConvention > iterator = set.iterator(); iterator.hasNext(); )
            {
                CallingConvention callingConvention = iterator.next();
                CallingConvention actual = getActualCallingConvention( callingConvention );
                if ( actual == null )
                {
                    if ( callingConvention.isGeneric() )
                    {
                        throw new IllegalStateException( "Platform failed to return actual calling convention for " + callingConvention );
                    }
                    set.remove( callingConvention );
                }
                else if ( actual.isGeneric() )
                {
                    throw new IllegalStateException( "Platform returned generic calling convention " + actual + " for " + callingConvention );
                }
            }

            set = Collections.unmodifiableSet( set );
            this.supportedCallingConventions = set;
        }
        return set;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public final int getPageSize()
    {
        int local = this.pageSize;
        if ( local == 0 )
        {
            local = PlatformUtilities.getPageSize();
            this.pageSize = local;
        }
        return local;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public final Pointer allocatePages( final int size,
                                        final boolean executable,
                                        final boolean writable )
    {
        DniInternal.LOGGER.fine( "Allocating pages of size 0x" + Integer.toHexString( size ) );

        long address = PlatformUtilities.allocatePages( size, pageFlags( executable, writable ) );
        if ( address == 0 )
        {
            throw new OutOfMemoryError( "Cannot allocate pages" );
        }

        Pointer pointer = Pointer.fromAddress( address );
        DniInternal.LOGGER.fine( "Allocated pages at " + pointer + " + 0x" + Integer.toHexString( size ) );
        return pointer;
    }

    /** {@inheritDoc} */
    @Override
    public void protectPages( final Pointer pointer,
                              final int size,
                              final boolean executable,
                              final boolean writable )
    {
        int code = PlatformUtilities.protectPages( pointer.address(),
                                                   size,
                                                   pageFlags( executable, writable ) );
        if ( code != 0 )
        {
            throw new SystemException( code );
        }
    }

    /** {@inheritDoc} */
    @Override
    public void unallocatePages( final Pointer pointer, final int size )
    {
        DniInternal.LOGGER.fine( "Unallocating pages at " + pointer + " + 0x" + Integer.toHexString( size ) );
        int code = PlatformUtilities.unallocatePages( pointer.address(), size );
        if ( code != 0 )
        {
            throw new AssertionError( "Failed to unallocate pages at " + pointer + " + 0x" + Integer.toHexString( size ) + ": " + code );
        }
    }

    /**
     * @return the default charset
     */
    abstract String getDefaultCharsetImpl();

    private static int pageFlags( final boolean executable, final boolean writable )
    {
        int flags = 0;
        if ( executable )
        {
            flags |= PlatformUtilities.PAGE_EXECTUABLE;
        }
        if ( writable )
        {
            flags |= PlatformUtilities.PAGE_WRITABLE;
        }
        return flags;
    }

}
